
/****************************************************************************/
/* Copyright 2008 Jesse E. H-K                                              */
/*                                                                          */
/* This file is part of Icepac.                                             */
/*                                                                          */
/* Icepac is free software: you can redistribute it and/or modify           */
/* it under the terms of the GNU General Public License as published by     */
/* the Free Software Foundation, either version 3 of the License, or        */
/* (at your option) any later version.                                      */
/*                                                                          */
/* Icepac is distributed in the hope that it will be useful,                */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            */
/* GNU General Public License for more details.                             */
/*                                                                          */
/* You should have received a copy of the GNU General Public License        */
/* along with Icepac.  If not, see <http://www.gnu.org/licenses/>.          */
/****************************************************************************/

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>
#include <locale.h>
#include <libintl.h>
#include <errno.h>

#include <libgen.h>
#include <argp.h>
#include <sys/stat.h>
#include <time.h>

#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

/* Define a shortcut for defining gettext-able strings. */
#define _( str ) gettext( str )

/*
 * The global Lua state that is used by the
 * entire program.
 */
static lua_State *L = NULL;

/*
 * These important constants dictate important limits on the
 * storage space available for certain operations or fields.
 */

/* The maximum buffer size for input. */
#define ICEPAC_BUFF_SIZE 256

/* The maximum number of characters in a directory path. */
#define ICEPAC_DIR_MAX 512

/* The maximum number of characters in a package name. */
#define ICEPAC_PKG_MAX 128

/* The maximum number of matching packages in a list. */
#define ICEPAC_RESULTS_MAX 50000

/* The maximum number of listed packages to install n one invocation of icepac. */
#define ICEPAC_PKG_ARGS_MAX 50

/*
 * These important constants dictate the location of
 * necessary files for the operation of icepc.
 */
#define ICEPAC_CONFIG_FILE ".icepac"
#define ICEPAC_LOGIC_FILE "icepac.lua"

/*
 * These represent the equivalent entries
 * in ~/.icepac.
 */
static char ABS_ROOT[ICEPAC_DIR_MAX];
static char LOCAL_DIR[ICEPAC_DIR_MAX];

/*************************************************
 * Utility functions                             *
 *************************************************/

/*
 * Print a formatted error message and quit.
 */
static void error( const char *fmt, ... ) {
    va_list argp;
    va_start( argp, fmt );

    if ( fmt != NULL ) {
        fprintf( stderr, PACKAGE ": " );
        vfprintf( stderr, fmt, argp );
    }

    va_end( argp );
    exit( EXIT_FAILURE );
}

/*
 * A quick addition to malloc to support rudimentory
 * error handling.
 */
static void *xmalloc( size_t bytes ) {
    void *m = malloc( bytes );

    if ( m == NULL )
        error( "%s\n", _( "Memory allocation error\n" ) );

    return m;
}

/*
 * Set <pkg>.<item> to <value> assuming all three are strings.
 * 
 * example use: set package.path to "/home/joe"
 * setfield( "package", "path", "/home/jesse" )
 */
static void setfield( const char *pkg, const char *key, const char *value ) {
    lua_getglobal( L, pkg );    
    lua_pushstring( L, key );
    lua_pushstring( L, value );
    lua_settable( L, -3 );
}

/*
 * Set the package path as appropriate
 * for the type of build environment.
 */
static void setpath( void ) {
    char buffer[ICEPAC_BUFF_SIZE];

    if ( LOCAL_BUILD ) {
        sprintf( buffer, "%s;%s", "./?.lua", "./?" );
        setfield( "package", "cpath", "./lib/.libs/?.so" );
        setfield( "package", "path", buffer );
    }
}

/*
 * Return the number of digits in an integer.
 *
 * This is mostly a helper function to improve the
 * formatting of the package list.
 */
static int getwidth( int numbresults) {
    int count = 1;

    while ( numbresults >= 10 ) {
        ++count;
        numbresults /= 10;
    }

    return count;
}

/* A Quick helper function to display
 * a yes/no style prompt with appropriate error checking.
 *
 * If possible, this function will perform a locale-appropriate
 * comparison. Otherwise, a case-insensitive 'y' will indicate an affirmative
 * response.
 *
 * On an affirmative response, a 1 is returned. An a negative response,
 * a 0 is returned.
 */
static int confirm( const char *prompt ) {
    char buff[ICEPAC_BUFF_SIZE];
    int response;


    while ( 1 ) {
        printf( "%s", prompt );
        if ( fgets( buff, ICEPAC_BUFF_SIZE, stdin ) == NULL ) {
            error( _( "Error reading input\n" ) );
        }

#if _SVID_SOURCE
        response = rpmatch( buff );
#else
        response = toupper( buff[0] ) == 'Y';
#endif
        if ( response == -1 ) {
            printf( _( "Invalid response\n" ) );
            continue;
        } else
            return response;
    }
}

/*************************************************
 * END Utility functions                         *
 *************************************************/

/*************************************************
 * Package List Functions and Implementation     *
 *************************************************/

/*
 * A single package.
 * Contains information on
 * its index in the search query,
 * its location in the filesystem,
 * and its name.
 */
typedef struct pnode {
    int index;               		/* Index among search results */
    char loc[ICEPAC_DIR_MAX];    	/* Location in filesystem */
    char name[ICEPAC_PKG_MAX];   	/* Package name */

    struct pnode *next;      		/* IMPLEMENTATION: Next package node */
    int _used : 1;           		/* IMPLEMENTATION: Tag for memory allocation */
} pnode;

/* Memory for the list of matching packages. */
static pnode sharedmem[ICEPAC_RESULTS_MAX];

/* 
 * Allocate memory for a single package node.
 * Handles all memory management internally using a simple algorithm.
 * Memory no longer in used will be recycled.
 */
static pnode *allocator( void ) {
    int i;

    for ( i = 0; i < ICEPAC_RESULTS_MAX; i++ ) {
        if ( ! sharedmem[i]._used) {
            sharedmem[i]._used = 1;
            return &(sharedmem[i]);
        }
    }

    error( _( "The number of matching packages has exceed the maximum of %d\n" ),
            ICEPAC_RESULTS_MAX );
}

static pnode *pnode_make( int index, const char *loc, const char *name, pnode *next ) {
    pnode *n = allocator();

    n->index = index;
    strcpy( n->loc, loc );
    strcpy( n->name, name );

    n->next = next;
    return n;
}

static pnode *pnode_destroy( pnode *node ) {
    pnode *next = node->next;
    node->_used = 0;

    return next;
}

/*
 * List of indexed packages.
 */
typedef struct plist {
    pnode *head;      /* Head of the list */
    pnode *tail;      /* Tail of the list (the last element before NULL) */
    int ip;           /* Maintained index */
} plist;

/* A few macros to ease working with package lists/nodes */
#define PNEXT( n ) ((n)->next)
#define PEND() NULL
#define PBEGIN( l ) ((l)->head)
#define PCOUNT( l ) ((l)->ip)

#define PINDEX( n ) ((n)->index)
#define PLOC( n ) ((n)->loc)
#define PNAME( n ) ((n)->name)

static plist *plist_make( void ) {
    plist *l = xmalloc( sizeof *l );

    l->head = NULL;
    l->tail = NULL;
    l->ip = 1;
    return l;
}

/*
 * Append a package to the list. This should happen in constant
 * time. If the list is empty, this function will create the first element as
 * appropriate.
 */
static void plist_add( plist *l, const char *loc, const char *name ) {
    if ( l->head == NULL ) {
        l->head = pnode_make( l->ip++, loc, name, NULL );
        l->tail = l->head;
    } else {
        l->tail->next = pnode_make( l->ip++, loc, name, NULL );
        l->tail = l->tail->next;
    }
}

/*
 * Free the memory associated with a package list.
 */
static void plist_destroy( plist *l ) {
    pnode *iter = NULL;

    iter = l->head;
    while ( iter != NULL )
        iter = pnode_destroy( iter );

    free( l );
}

/*************************************************
 * END Package List Functions and Implementation *
 *************************************************/

/*
 * Initiate a call to a function in the icepac library
 * where "name" refers to function "icepac.<name>"
 * by pushing it onto the global lua stack.
 */
static void initfunc( const char *name ) {
    lua_getglobal( L, "icepac" );
    lua_pushstring( L, name );
    lua_gettable( L, -2 );
}

/*
 * Check to make sure that
 * the package tree location specified in
 * the configuration file contains all the
 * repositories specified.
 * 
 * Returns true or false.
 */
static int checktree( void ) {
    initfunc( "checktree" );

    if ( lua_pcall( L, 0, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );

    return lua_toboolean( L, -1 );
}

/*
 * Calls icepac.lua in order to search for a package by name.
 */
static const char *locatepackage( const char *name ) {
    initfunc( "locate" );
    lua_pushstring( L, name );

    if ( lua_pcall( L, 1, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );

    if ( lua_isnil( L, -1 ) )
        return NULL;
    else
        return lua_tostring( L, -1 );
}   

/*
 * Return a plist based on matches to the
 * "desired" pattern.
 *
 * Calls icepac.search and creates a package list
 * from the resulting Lua table of matches.
 */
static plist *searchpackage( const char *desired ) {
    const char *ent = NULL;
    char buff[ICEPAC_DIR_MAX];
    plist *pl = NULL;

    initfunc( "search" );
    lua_pushstring( L, desired );

    if ( lua_pcall( L, 1, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );

    if ( lua_isnil( L, -1 ) )
        return NULL;

    pl = plist_make();
    /* Start with a nil. This is convention. */
    lua_pushnil( L );
    while ( lua_next( L, -2 ) ) {
        ent = luaL_checkstring( L, -1 );
        strcpy( buff, ent );

        plist_add( pl, ent, basename( buff ) );
        /* Leave the key on the stack for the next round,
         * but pop the value. */
        lua_pop( L, 1 );
    }

    /* Pop the list of entries */
    lua_pop( L, 1 );
    return pl;
}

#define STRINGIFY( var ) #var

#define checktable( table )                                                 \
        {                                                                   \
    lua_getglobal( L, STRINGIFY( table ) );                                 \
    if ( ! lua_istable( L, -1 ) )                                           \
    error( _( "Table expected for %s in ~/.icepac\n" ), STRINGIFY( table ) );     \
        }

#define checkstring( var )                                                  \
        {                                                                   \
    lua_getglobal( L, STRINGIFY( var ) );                                   \
    if ( ! lua_isstring( L, -1 ) )                                          \
    error( _( "String expected for %s in ~/.icepac\n" ), STRINGIFY( var ) );     \
    strcpy( var, lua_tostring( L, -1 ) );                                   \
        }

#define checkdir( var )                                                     \
        {                                                                   \
    initfunc( "isdir" );                                                    \
    checkstring( var );                                                     \
    if ( lua_pcall( L, 1, 1, 0 ) || ! lua_toboolean( L, -1 ) )              \
    error( _( "%s in ~/.icepac is not a valid directory\n" ), STRINGIFY( var ) );   \
        }

/*
 * Create a default configuration file with some default
 * values if one doesn't exist already.
 */
static void createdefaultconfig( char *name ) {
    FILE *cf = NULL;
    time_t currtime;
    struct tm *loctime;

    if ( (cf = fopen( name, "w" )) == NULL ) {
        error( _( "Cannot create %s: %s\n" ), name, strerror( errno ) );
    }

    currtime = time( NULL );
    loctime = localtime( &currtime );

    fprintf( cf,
            "-- ~/.icepac\n"
            "--\n"
            "-- This is the configuration file for icepac.\n"
            "-- It was created automatically on\n"
            "-- %s\n",
            asctime( loctime ) );

    fprintf( cf,
            "-- A list of repositories in the ABS tree\n"
            "-- while will be included when searching for\n"
            "-- packages.\n"
            "REPOS = {\n"
            "    \"core\",\n"
            "    \"extra\",\n"
            "    \"community\",\n"
            "}\n\n"
            "-- The path to the root of the ABS tree.\n"
            "ABS_ROOT = \"/var/abs\"\n\n"
            "-- The path to the directory where you wish to\n"
            "-- store your packages.\n"
            "LOCAL_DIR = \"%s/abs\"\n",
            getenv( "HOME" )
            );

    fclose( cf );
}

/*
 * Read the icepac configuration file in ~/.icepac
 * and check the format of the necessary variables.
 *
 * On error, an appropriate message will be displayed and
 * the program will quit.
 */
static void readconfig( void ) {
    char buffer[ICEPAC_BUFF_SIZE];
    struct stat s;

    sprintf( buffer, "%s/%s", getenv( "HOME" ), ICEPAC_CONFIG_FILE );

    /*
     * First, the existance of the file is confirmed. If it doesn't
     * exist, then it will be created with some default values.
     */
    if ( stat( buffer, &s ) ) {
        createdefaultconfig( buffer );
    }

    if ( luaL_dofile( L, buffer ) ) {
        error( "%s\n", lua_tostring( L, -1 ) );
    }

    checkdir( ABS_ROOT );
    checkstring( LOCAL_DIR );
    checktable( REPOS );
}

/*
 * Calls icepac.cppackage to copy a matching
 * package from one directory to another as specified by
 * the user.
 */
static const char *cppackage( const char *name, const char *loc ) {
    initfunc( "cppackage" );

    lua_pushstring( L, name );
    lua_pushstring( L, loc );

    if ( lua_pcall( L, 2, 1, 0 ) )
        error( "%s\n", lua_tostring( L, -1 ) );

    return lua_tostring( L, -1 );
}

#if LOCAL_BUILD
#  define LOGIC_LOC "."
#else
#  define LOGIC_LOC LUA_LDIR
#endif

#define ABOUT_STR \
        "\nThis is " PACKAGE_STRING "\n"            \
        "Copyright 2008 Jesse E. H-K\n\n"

static char args_doc[] = "<package or pattern>";

static struct argp_option options[] = {
        { "search", 's', 0, 0, "Search mode" },
        { "noconfirm", 'n', 0, 0, "Don't confirm actions" },
        { "quiet", 'q', 0, 0, "Don't print any output. Can be compined with --noconfirm" },
        { 0 },
};

struct arguments {
    int search;
    int noconfirm;
    int quiet;

    char *args[ICEPAC_PKG_ARGS_MAX];
    int numbpkg;
};

/*
 * Parse the command-line arguments and set the parameters
 * of the arguments struct as necessary.
 */
static error_t parseopt( int key, char *arg, struct argp_state *state ) {
    struct arguments *arguments = state->input;

    switch ( key ) {
    case 's':
        arguments->search = 1;
        break;
    case 'n':
        arguments->noconfirm = 1;
        break;
    case 'q':
        arguments->quiet = 1;
        break;
    case ARGP_KEY_ARG:
        if ( ((state->arg_num >= 1) && arguments->search) || (state->arg_num > ICEPAC_PKG_ARGS_MAX) )
            /* Too many arguments. */
            argp_usage( state );

        arguments->args[state->arg_num] = arg;

        break;
    case ARGP_KEY_END:
        /* Not enough arguments! */
        if ( state->arg_num < 1 )
            argp_usage( state );

        break;
    default:
        return ARGP_ERR_UNKNOWN;
    }

    arguments->numbpkg = state->arg_num;
    return 0;
}

static struct argp argp = { options, parseopt, args_doc, ABOUT_STR };

/* This is the global package list for every invocation of
 * of the program.
 */
static plist *PKGL = NULL;

/*
 * On the program exit, this function should be
 * called to close the resources used.
 *
 * These are namely the lua instance and the package list.
 */
static void cleanup( void ) {
    lua_close( L );

    if ( PKGL )
        plist_destroy( PKGL );
}

/*
 * One of two major modes for icepac.
 *
 * This function populates the package matching list
 * and formats the results as a numbered list formatted appropriately.
 *
 * If there are no matches, then this function simply returns without printing
 * any results.
 */
static void searchmode( const char *pattern ) {
    pnode *iter = NULL;
    int width;

    if ( (PKGL = searchpackage( pattern )) == NULL )
        return;

    width = getwidth( PCOUNT( PKGL ) );
    iter = PBEGIN( PKGL );
    for (; iter != PEND(); iter = PNEXT( iter ) )
        printf( "%-*d: %s\n", width, PINDEX( iter ), PNAME( iter ) );
}   

/*
 * One of two major modes for icepac.
 *
 * This function attempts to find an exact match
 * for a package name specified in the arguments parameters
 * and copies the package to the user-specified directory.
 */
static void installmode( struct arguments *arguments ) {
    const char *pkgname = NULL;
    const char *loc = NULL;
    const char *savedloc = NULL;
    char buff[ICEPAC_BUFF_SIZE];
    int i;

    for ( i = 0; i < arguments->numbpkg; i++ ) {
        pkgname = arguments->args[i];
        loc = locatepackage( pkgname );
        if ( loc == NULL )
            error( _( "No such package: %s\n" ), pkgname );

        if ( ! arguments->noconfirm ) {
            sprintf( buff, _( "Copy %s to local directory? [y/N] " ), pkgname );
            if ( ! confirm( buff ) )
                continue;
        }

        savedloc = cppackage( pkgname, loc );
        if ( ! arguments->quiet )
            printf( _( "%s copied from %s to %s\n" ), pkgname, loc, savedloc );
    }
}

int main( int argc, char **argv ) {
    struct arguments arguments;    

    /* Initialize the option defaults. */
    arguments.search = 0;
    arguments.noconfirm = 0;
    arguments.quiet = 0;

    /* Set the locale appropriately. */
    setlocale( LC_ALL, "" );
    bindtextdomain( PACKAGE, LOCALEDIR );
    textdomain( PACKAGE );

    /* Initialize the main Lua instance. */
    L = lua_open();

    /* 
     * Make sure the Lua instance is destroyed
     * when the program exits from now on.
     */
    atexit( cleanup );

    luaL_openlibs( L );    

    /* Set the correct package path. */
    setpath();

    if ( luaL_dofile( L, LOGIC_LOC "/icepac.lua" ) )
        error( _( "Unable to read necessary file icepac.lua\n"
                "%s\n" ), lua_tostring( L, -1 ) );


    /* Read config file and set important vars. */
    readconfig();

    /*
     * Check the abs tree for all the repositories mentioned in the
     * configuration file.
     */
    if ( ! checktree() )
        error( _( "Unable to locate a valid ABS tree at existing directory %s\n"
                "Some repositories specified in ~/.icepac are not present\n" ), ABS_ROOT );

    argp_parse( &argp, argc, argv, 0, 0, &arguments );    

    /*
     * Based on the command line switches, execute the
     * chosen function.
     */
    if ( arguments.search )
        searchmode( arguments.args[0] );
    else
        installmode( &arguments );

    return 0;
}
